// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.guing.common.components.properties.panel;

import static java.util.Objects.requireNonNull;

import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.RowSorter;
import javax.swing.SortOrder;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableRowSorter;
import org.opentcs.data.order.OrderConstants;
import org.opentcs.guing.base.components.properties.type.OrderTypesProperty;
import org.opentcs.guing.base.components.properties.type.Property;
import org.opentcs.guing.base.model.AcceptableOrderTypeModel;
import org.opentcs.guing.common.components.dialogs.DetailsDialogContent;
import org.opentcs.guing.common.components.dialogs.StandardContentDialog;
import org.opentcs.guing.common.transport.OrderTypeSuggestionsPool;
import org.opentcs.guing.common.util.I18nPlantOverview;
import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil;

/**
 * User interface to edit a set of order types.
 */
public class OrderTypesPropertyEditorPanel
    extends
      JPanel
    implements
      DetailsDialogContent {

  /**
   * The bundle to be used.
   */
  private final ResourceBundleUtil bundle
      = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH);
  /**
   * The pool of types to suggest.
   */
  private final OrderTypeSuggestionsPool typeSuggestionsPool;
  /**
   * The property to edit.
   */
  private OrderTypesProperty fProperty;

  /**
   * Creates a new instance.
   *
   * @param typeSuggestionsPool The pool of types to suggest.
   */
  @Inject
  @SuppressWarnings("this-escape")
  public OrderTypesPropertyEditorPanel(OrderTypeSuggestionsPool typeSuggestionsPool) {
    this.typeSuggestionsPool = requireNonNull(typeSuggestionsPool, "typeSuggestionsPool");
    initComponents();
    initTable();
  }

  @Override
  public void setProperty(Property property) {
    fProperty = (OrderTypesProperty) property;
    getTableModel().setValues(fProperty.getItems());
  }

  @Override
  public void updateValues() {
    fProperty.setItems(getTableModel().getValues());
  }

  @Override
  public String getTitle() {
    return bundle.getString("orderTypesPropertyEditorPanel.title");
  }

  @Override
  public Property getProperty() {
    return fProperty;
  }

  private void initTable() {
    TableRowSorter<OrderTypeTableModel> sorter = new TableRowSorter<>(getTableModel());
    // Explicitly override the default comparator for the "name" column, which otherwise would be
    // a locale-specific Collator instance, which does not necessarily sort in natural order.
    sorter.setComparator(OrderTypeTableModel.COLUMN_NAME, Comparator.naturalOrder());
    // Sort the table first by order type priority and then by order type name...
    sorter.setSortKeys(
        Arrays.asList(
            new RowSorter.SortKey(OrderTypeTableModel.COLUMN_PRIORITY, SortOrder.ASCENDING),
            new RowSorter.SortKey(OrderTypeTableModel.COLUMN_NAME, SortOrder.ASCENDING)
        )
    );
    // ...but prevent manual sorting.
    for (int i = 0; i < orderTypesTable.getColumnCount(); i++) {
      sorter.setSortable(i, false);
    }
    sorter.setSortsOnUpdates(true);
    orderTypesTable.setRowSorter(sorter);
  }

  private OrderTypeTableModel getTableModel() {
    return (OrderTypeTableModel) orderTypesTable.getModel();
  }

  private boolean orderTypeExistsInTable(AcceptableOrderTypeModel orderType) {
    return getTableModel().getValues().stream()
        .anyMatch(tableType -> Objects.equals(tableType.getName(), orderType.getName()));
  }

  // FORMATTER:OFF
  // CHECKSTYLE:OFF
  /**
   * This method is called from within the constructor to initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is always
   * regenerated by the Form Editor.
   */
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {
    java.awt.GridBagConstraints gridBagConstraints;

    orderTypesScrollPane = new javax.swing.JScrollPane();
    orderTypesTable = new javax.swing.JTable();
    controlPanel = new javax.swing.JPanel();
    addButton = new javax.swing.JButton();
    editButton = new javax.swing.JButton();
    removeButton = new javax.swing.JButton();
    controlFiller = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0));

    setPreferredSize(new java.awt.Dimension(300, 250));
    setLayout(new java.awt.GridBagLayout());

    orderTypesTable.setModel(new OrderTypeTableModel());
    orderTypesScrollPane.setViewportView(orderTypesTable);

    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 0;
    gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
    gridBagConstraints.weightx = 1.0;
    gridBagConstraints.weighty = 1.0;
    add(orderTypesScrollPane, gridBagConstraints);

    controlPanel.setLayout(new java.awt.GridBagLayout());

    addButton.setFont(addButton.getFont());
    java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N
    addButton.setText(bundle.getString("orderTypesPropertyEditorPanel.button_add.text")); // NOI18N
    addButton.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        addButtonActionPerformed(evt);
      }
    });
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 0;
    gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
    gridBagConstraints.insets = new java.awt.Insets(0, 3, 3, 0);
    controlPanel.add(addButton, gridBagConstraints);

    editButton.setText(bundle.getString("orderTypesPropertyEditorPanel.button_edit.text")); // NOI18N
    editButton.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        editButtonActionPerformed(evt);
      }
    });
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 1;
    gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
    gridBagConstraints.insets = new java.awt.Insets(0, 3, 3, 0);
    controlPanel.add(editButton, gridBagConstraints);

    removeButton.setFont(removeButton.getFont());
    removeButton.setText(bundle.getString("orderTypesPropertyEditorPanel.button_remove.text")); // NOI18N
    removeButton.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        removeButtonActionPerformed(evt);
      }
    });
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 2;
    gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
    gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0);
    controlPanel.add(removeButton, gridBagConstraints);
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 3;
    gridBagConstraints.weightx = 1.0;
    gridBagConstraints.weighty = 1.0;
    controlPanel.add(controlFiller, gridBagConstraints);

    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL;
    add(controlPanel, gridBagConstraints);
  }// </editor-fold>//GEN-END:initComponents
  // CHECKSTYLE:ON
  // FORMATTER:ON

  private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed
    int selectedRow = orderTypesTable.getSelectedRow();
    if (selectedRow == -1) {
      return;
    }

    getTableModel().remove(orderTypesTable.convertRowIndexToModel(selectedRow));
  }//GEN-LAST:event_removeButtonActionPerformed

  private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed
    OrderTypePanel content = new OrderTypePanel(
        new AcceptableOrderTypeModel(OrderConstants.TYPE_ANY, 0),
        typeSuggestionsPool
    );
    StandardContentDialog dialog = new StandardContentDialog(this, content);
    dialog.setLocationRelativeTo(this);
    dialog.setVisible(true);

    if (dialog.getReturnStatus() != StandardContentDialog.RET_OK) {
      return;
    }

    AcceptableOrderTypeModel newType = content.getAcceptableOrderTypeModel();
    if (orderTypeExistsInTable(newType)) {
      JOptionPane.showMessageDialog(
          this,
          bundle.getString(
              "orderTypesPropertyEditorPanel.optionPane_typeAlreadyPresentError.message"
          )
      );
      return;
    }

    getTableModel().add(newType);
    typeSuggestionsPool.addTypeSuggestion(newType.getName());
  }//GEN-LAST:event_addButtonActionPerformed

  private void editButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editButtonActionPerformed
    int selectedRow = orderTypesTable.getSelectedRow();
    if (selectedRow == -1) {
      return;
    }

    AcceptableOrderTypeModel oldType = getTableModel().getValueAt(
        orderTypesTable.convertRowIndexToModel(selectedRow)
    );
    OrderTypePanel content = new OrderTypePanel(oldType, typeSuggestionsPool);
    StandardContentDialog dialog = new StandardContentDialog(this, content);
    dialog.setLocationRelativeTo(this);
    dialog.setVisible(true);

    if (dialog.getReturnStatus() != StandardContentDialog.RET_OK) {
      return;
    }

    // First, remove the old type.
    getTableModel().remove(orderTypesTable.convertRowIndexToModel(selectedRow));

    // Then, check if a type with the new type's name already exists.
    AcceptableOrderTypeModel newType = content.getAcceptableOrderTypeModel();
    if (orderTypeExistsInTable(newType)) {
      // If a type with the new type's name already exists, restore the old type.
      getTableModel().add(oldType);
      JOptionPane.showMessageDialog(
          this,
          bundle.getString(
              "orderTypesPropertyEditorPanel.optionPane_typeAlreadyPresentError.message"
          )
      );
      return;
    }

    getTableModel().add(newType);
    typeSuggestionsPool.addTypeSuggestion(newType.getName());
  }//GEN-LAST:event_editButtonActionPerformed

  // FORMATTER:OFF
  // CHECKSTYLE:OFF
  // Variables declaration - do not modify//GEN-BEGIN:variables
  private javax.swing.JButton addButton;
  private javax.swing.Box.Filler controlFiller;
  private javax.swing.JPanel controlPanel;
  private javax.swing.JButton editButton;
  private javax.swing.JScrollPane orderTypesScrollPane;
  private javax.swing.JTable orderTypesTable;
  private javax.swing.JButton removeButton;
  // End of variables declaration//GEN-END:variables
  // CHECKSTYLE:ON
  // FORMATTER:ON

  private class OrderTypeTableModel
      extends
        AbstractTableModel {

    /**
     * The number of the "Name" column.
     */
    public static final int COLUMN_NAME = 0;
    /**
     * The number of the "Priority" column.
     */
    public static final int COLUMN_PRIORITY = 1;
    /**
     * The column names.
     */
    private final String[] columnNames
        = new String[]{
            bundle.getString(
                "orderTypesPropertyEditorPanel.table_orderTypes.column_name.headerText"
            ),
            bundle.getString(
                "orderTypesPropertyEditorPanel.table_orderTypes.column_priority.headerText"
            )
        };
    /**
     * Column classes.
     */
    private final Class<?>[] columnClasses
        = new Class<?>[]{
            String.class,
            Integer.class
        };
    /**
     * The values in this model.
     */
    private final List<AcceptableOrderTypeModel> values = new ArrayList<>();

    OrderTypeTableModel() {
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
      return columnClasses[columnIndex];
    }

    @Override
    public String getColumnName(int columnIndex) {
      return columnNames[columnIndex];
    }

    @Override
    public int getRowCount() {
      return values.size();
    }

    @Override
    public int getColumnCount() {
      return columnNames.length;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
      if (!rowInBounds(rowIndex)) {
        return null;
      }

      AcceptableOrderTypeModel entry = values.get(rowIndex);
      switch (columnIndex) {
        case COLUMN_NAME:
          return entry.getName();
        case COLUMN_PRIORITY:
          return entry.getPriority();
        default:
          throw new IllegalArgumentException("Invalid column index: " + columnIndex);
      }
    }

    public void setValues(Collection<AcceptableOrderTypeModel> values) {
      requireNonNull(values, "values");

      this.values.clear();
      this.values.addAll(values);
      fireTableDataChanged();
    }

    public List<AcceptableOrderTypeModel> getValues() {
      return Collections.unmodifiableList(values);
    }

    public AcceptableOrderTypeModel getValueAt(int row) {
      return values.get(row);
    }

    public boolean add(AcceptableOrderTypeModel envelopeModel) {
      values.add(envelopeModel);
      fireTableRowsInserted(values.size() - 1, values.size() - 1);
      return true;
    }

    public boolean remove(int row) {
      if (!rowInBounds(row)) {
        return false;
      }

      values.remove(row);
      fireTableRowsDeleted(row, row);
      return true;
    }

    private boolean rowInBounds(int row) {
      if (values.isEmpty()) {
        return false;
      }

      return row >= 0 && row <= values.size() - 1;
    }
  }
}
